Skip to content

[EXP-23262]: S7: support subrepo cloning via GH_USER / GH_TOKEN#92

Merged
oleksandrhleba merged 3 commits into
mainfrom
task/EXP-23262-github-token-auth
Jul 7, 2026
Merged

[EXP-23262]: S7: support subrepo cloning via GH_USER / GH_TOKEN#92
oleksandrhleba merged 3 commits into
mainfrom
task/EXP-23262-github-token-auth

Conversation

@oleksandrhleba

@oleksandrhleba oleksandrhleba commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Ticket link

EXP-23262

Summary

Adds native support in s7 for authenticating GitHub subrepo network operations over HTTPS with a Personal Access Token, instead of relying on an SSH key.

When S7_GIT_USER + S7_GIT_TOKEN (or the GH_USER / GH_TOKEN fallback) are present, s7 makes git rewrite SSH github.com URLs to HTTPS and supplies the PAT as an HTTP Authorization header. When they're absent, behavior is unchanged — SSH as before.

How it works

All git invocations funnel through +[GitRepository runGitWithArguments:…]. When both credentials are present, s7 injects three GIT_CONFIG_* entries into the child git process's environment:

insteadOf moves the subrepo URL onto HTTPS transport; the github.com‑scoped extraheader authenticates it — the same approach actions/checkout uses. This is git ≥ 2.31's env-based config; no git config file is touched.

Security properties

  • Token never in a URL — it rides in an HTTP header, so it can't leak through git error messages, GIT_TRACE_CURL, or remote.origin.url. base64 (not percent-encoded creds in a URL) also means arbitrary token bytes are safe.
  • Token never in argv — delivered via environment, not -c, so it can't appear in ps / /proc / crash reports.
  • Token never on disk — .git/config and .s7substate keep the original SSH URL; the rewrite is resolution-time only.
  • Header scoped to github.com — never attached to a redirect or other host.

Behavior details

  • No-op off the token path. With no credentials, task.environment is left untouched, so dev machines and SSH users are completely unaffected.
  • Composes with inherited config. New entries append past any existing GIT_CONFIG_COUNT, so a nested s7 (which inherits the parent's injected config) or any other tool's GIT_CONFIG_* is never clobbered.
  • Diagnostic. On the token path, s7 prints s7: subrepo network auth: HTTPS via token to stderr (once) so CI auth failures are easy to spot.

Testing

  • Unit tests (gitGitHubTokenAuthTests.m): the config builder produces the correct entries, appends past an existing count, and the raw token never appears outside the base64 header. Builds clean under -Werror.
  • Manually verified end-to-end against a real PAT on a clone of rd2 (SSH disabled via GIT_SSH_COMMAND=false to prove the HTTPS path).

Scope & follow-ups

  • No automated CI smoke against a live PAT yet — recommend adding a gated git ls-remote check.

@oleksandrhleba oleksandrhleba requested review from a-olkhovskyi and pastey and removed request for a-olkhovskyi June 30, 2026 16:09

@pastey pastey left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not sure if we need GH_USER/GH_TOKEN in the s7 code. What are your thoughts?

Comment thread system7/git/Git.m
Comment thread system7/git/Git.m
Comment thread system7/git/Git.m Outdated
Comment thread system7/git/Git.m Outdated
Comment thread system7/git/Git.m Outdated
Comment thread system7/git/Git.m Outdated
// GIT_CONFIG_COUNT) doesn't clobber it.
+ (NSDictionary<NSString *, NSString *> *)gitHubTokenConfigEnvironmentForUser:(nullable NSString *)user
token:(nullable NSString *)token
existingConfigCount:(NSInteger)existingConfigCount

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of passing existingConfigCount argument, I would directly access the

NSDictionary<NSString *, NSString *> *const env = NSProcessInfo.processInfo.environment;
        const NSInteger existingConfigCount = MAX(0, [env[@"GIT_CONFIG_COUNT"] integerValue]);

here. This way all the logic managing GIT_CONFIG_COUNT would be in a single place. Now it's spread out by two methods.
Plus NSInteger seems odd. Why not unsigned?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point. Fixed with one small deviation: reading NSProcessInfo inside the builder would kill the tests, so instead the builder takes the environment as a parameter.

Comment thread system7/git/Git.m Outdated
Comment thread system7/git/Git.m Outdated
@oleksandrhleba

oleksandrhleba commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

I'm still not sure if we need GH_USER/GH_TOKEN in the s7 code. What are your thoughts?

I've added those since they're used on Jenkins rn. And as far as I understand - Jenkins will use one PAT for all. So with current state of Jenkins we will have GH_* and S7_GIT_* values duplicated.

S7_GIT_* are working in parallel and have higher priority. So in case if we will need to use separate token for S7 - we could.

TBH I don't see the point in having duplicated values for two keys. But I do see actual case when we would like to have S7 specific credentials.

In case if on system level we actually will retire GH_* env variable usage for all the project repos - then I'm fine to delete it :)

@oleksandrhleba oleksandrhleba requested a review from pastey July 6, 2026 14:20

@pastey pastey left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the changes. Looks almost perfect now 😍
Have few more suggestions 🙏

As for the GH_USER / S7_ – let it be. Duplicating the env variable in Jenkins configs will be too much of a pain indeed.

Comment thread system7/git/Git.m
}

NSMutableDictionary<NSString *, NSString *> *const result = [NSMutableDictionary new];
__block NSUInteger nextConfigPairIndex = (NSUInteger)MAX(0, [processEnvironment[@"GIT_CONFIG_COUNT"] integerValue]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
__block NSUInteger nextConfigPairIndex = (NSUInteger)MAX(0, [processEnvironment[@"GIT_CONFIG_COUNT"] integerValue]);
__block NSUInteger nextConfigPairIndex = MAX(0, [processEnvironment[@"GIT_CONFIG_COUNT"] unsignedIntegerValue]);

@oleksandrhleba oleksandrhleba Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

processEnvironment[@"GIT_CONFIG_COUNT"] returns string, not NSNumber, so I won't be able to use unsignedIntegerValue here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad. Thanks 👍

Comment thread system7/git/Git.m Outdated
Comment on lines +102 to +105
if (configEnvironment.count > 0) {
NSMutableDictionary<NSString *, NSString *> *const merged = [env mutableCopy];
[merged addEntriesFromDictionary:configEnvironment];
taskEnvironment = [merged copy];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to put all the merging logic into the gitHubTokenConfigEnvironmentForUser itself.
Now the return value of the gitHubTokenConfigEnvironmentForUser is inconsistent: at one hand, it contains just the values we want to add, but the GIT_CONFIG_COUNT is the sum of our custom values and the keys from the original environment. If we try to use gitHubTokenConfigEnvironmentForUser as it's written now in some other place, we'd have to add the same merge there too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plus, I would suggest to remove the call for copy.
- taskEnvironment = [merged copy];
+ taskEnvironment = merged;

I don't think we have some unscrupulous people casting NSDictionary to NSMutableDictionary around here.

Comment thread system7/git/Git.m Outdated
// keeps the original SSH URL, so the token never lands on disk either.
+ (nullable NSDictionary<NSString *, NSString *> *)gitHubTokenAuthTaskEnvironment {
static dispatch_once_t onceToken;
static NSDictionary<NSString *, NSString *> *taskEnvironment;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm always scared by uninitialised variable. Even the static ones.

Suggested change
static NSDictionary<NSString *, NSString *> *taskEnvironment;
static NSDictionary<NSString *, NSString *> *taskEnvironment = nil;

@oleksandrhleba oleksandrhleba requested a review from pastey July 7, 2026 14:03

@pastey pastey left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@oleksandrhleba oleksandrhleba merged commit a394423 into main Jul 7, 2026
3 checks passed
@oleksandrhleba oleksandrhleba deleted the task/EXP-23262-github-token-auth branch July 7, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants